fix(harness): a project row's two verbs are New agent and Remove, as hover actions [SAP-2981] - #842
Conversation
…menu The ⋮ on every project row opened a 248px card to hold one item. Its create item is suppressed wherever the Agent Map owns creation, which is every plan-first project, so on the product path the popover existed to carry a single "Remove <project> from the rail" — a card, an anchor and a min-width whose stated purpose in the CSS was fitting that one sentence on one line. Remove is now an X beside the session shortcut, hover-revealed like every other row action, opening the same RemoveProjectConfirm as before: the project named, the count of running sessions it ends, and the statement that nothing on disk is touched. The legacy create action, where a server still offers one, keeps its own glyph rather than a menu row. design-eng D33 (2026-09-04) already ruled this way and was not followed: "a project row's verbs are hover actions on the header ... a per-row menu would be a new idiom", rejecting "a ⋯ menu on every project row". Reversibility: low. Focus survives the change rather than degrading. The menu item that opened the confirm unmounted with its popover, so focus was released to <body>; the X is still mounted when the dialog closes, so focus returns to the control that opened it. dialog-shell's CreateAgentDialog case gains the trigger it could not have before. The collapsed-row exclusion in styles.css goes with the menu: it existed only because a menu trigger always carries aria-expanded, which made every collapsed project row wear a standing ⋮. Plain row actions carry no such attribute. The hover-reveal spec now asserts the contract across every action on the row instead of a hard-coded pair, so adding or removing a verb cannot leave one standing at full strength unnoticed. Verified: 544 playwright mock specs pass; web typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw
Creating an agent inside an existing project had no door left. `mapOwnsCreation`
is `studioProject != null`, and the comment above it says current servers issue
a durable Studio project for every scope — so it is true everywhere. It gated
both the row's create action and the empty project's create row, on the stated
grounds that "its pinned Agent Map is the only route to generating agents".
AgentMapPane has no create control. The route was the planner session driving
the map's MCP tools, which SAP-3143 deletes in this same branch. A project that
already holds agents was therefore left with nothing: the row offered a session
and a remove, the empty-project row never rendered, and the rail's top CTA opens
the composer with no project context, so it cannot create INTO a project.
The `+` is New agent again, which is also what the design has said since
2026-09-04 — design-eng IA.md 219 and D34(a) put `project-create-agent-{label}`
on the row itself, hover-revealed. A bare project keeps its distinct scaffold
verb, which grows an agent inside the session already running there.
`project-start-session-{label}` comes off the row. D34(e) and D35 item 6 give a
plain session to the tab strip, and SAP-3143 added the project pane's own Start
a session, which is the map-altitude path. `startProjectSession` in App.tsx is
untouched and still serves both.
The empty project still gets no create row of its own: D36 makes its Agent Map
row the CTA, and that gate is the one thing `mapOwnsCreation` still owns.
Verified: 544 playwright mock specs pass; web typecheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw
Three specs on this branch drive controls the two commits before this one
remove. None of them is about those controls; each reaches through one to get
somewhere else.
- `project-map-navigation` opened the ⋮ only to click Remove inside it. Remove
is a row action now, so the menu click goes and the Remove click stays.
- `project-axis` "a failed project session keeps the map and active
conversation intact" used the row's session `+`. That `+` is New agent. The
project pane's Start is not a substitute HERE: it is the no-session empty
state's CTA, and this scenario has a live session by design. A SECOND session
comes from the tab strip, which is where D34(e) and D35 item 6 put a plain
session to begin with.
- The same spec's row-action inventory now expects New agent plus Remove, and
asserts the session shortcut is absent rather than visible.
One behaviour difference is recorded rather than hidden: the tab strip reports
"Couldn't start the session." where the row `+` passed the server's own message
through ("mock: couldn't create session"). Less specific, and worth fixing
separately. The guarantee the test exists for is unchanged — a failed start
leaves the map and the active conversation exactly as they were.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw
Review — PR #842🔒 CONFIDENTIALITY — internal design-doc and roadmap references in both changesets
These are pointers into a private design doc with internal decision numbers. An npm consumer cannot resolve them and does not need to; they exist to satisfy an internal reviewer. The repo's precedent is a bare ticket id ( Same file, line 13: "its only route to generating agents was the planner session, which SAP-3143 removes" — publishing unshipped internal roadmap. State the behaviour that changed, not the ticket that will change it next. Also fold out the internal-implementation asides that only mean something to this repo: "a 248px card", "the Regression — legacy servers lose the only way to start a plain session in a project
So on a legacy server, a project with no session (e.g. the fixture's Either keep the row verb when Regression shipped knowingly — failed session start lost its server message
Semver —
|
Primary change type
Problem and motivation
Based on
fix/studio-onboarding-followups, notmain— this is the rail-grammar slice of the now-closed #837, rebased onto your branch. Everything else in #837 you had already done independently: the planner deletion, and un-gating creation. This is the part that existed nowhere else.A project row carries three controls acting on two different nouns. The
+starts a session; the⋮holds Create an agent and Remove the project. So the frequent build verb is buried in a popover while a session shortcut owns the+, and the overflow's own accessible name — "Actions for acme-app" — names no action at all.design-eng ruled on this on 2026-09-04, and the code has not followed:
DECISIONS.mdD33 (rev low)DECISIONS.mdD34(a)+keeps RAIL-REBUILD's placement and testid (project-create-agent-{label}) and now opens the new-agent screen"IA.md219+… hover-revealed like every row action"IA.md224, D34(e), D35 item 6D33's rationale names this branch's version explicitly: "The harness's row menu carries the verb because the harness rows have menus; v2's rows have actions."
Summary and scope
The row has two verbs, both hover actions, each naming its own subject.
+is New agent, scoped to the row's project (project-create-agent-{label}). A bare project keeps its distinct scaffold verb.RemoveProjectConfirmas before — project named, running sessions counted, "nothing on disk is touched".project-start-session-{label}is removed from the row. A plain session comes from the tab strip, or the project pane'sproject-start-sessionStart.ProjectRowMenuand its.project-row-menu248px card are deleted, along with the collapsed-row CSS exclusion that existed only because a menu trigger always carriesaria-expanded.Row actions before and after, read off the DOM:
The menu being removed, and the row after
⋮open+and×A 248px card for two items, whose CSS comment stated its own purpose: "Wide enough for the longest item to state its subject on one line."
The confirmation is unchanged and still reachable, now in one click:
How to review
Start at
WorkflowsRail.tsx—ProjectRowMenubecomesProjectRowActions, and the row's trailing block loses the session button. Focus handling improves: the menu item that opened the confirm unmounted with its popover, so focus was released to<body>; the X is still mounted, so focus returns to it.dialog-shell'sCreateAgentDialogcase gains thetriggerit could not previously have.The third commit re-points three of your specs that reached through the deleted controls to get somewhere else. None was about those controls.
One behaviour difference, recorded rather than hidden. In "a failed project session keeps the map and active conversation intact", the project pane's Start is not a substitute for the row
+: it is the no-session empty state's CTA, and that scenario has a live session by design. A second session now comes from the tab strip — which reports"Couldn't start the session."where the row+passed the server's message through ("mock: couldn't create session"). Less specific, worth fixing separately. The guarantee the test exists for is unchanged.Verification
Three specs failed only under full-suite load and pass in isolation —
canvas-inspector,popover-crop,project-map-navigation— the known flake band, all canvas-timing and none touching the rail.Local runs are the only evidence here.
test.ymlandharness.ymlarepull_request: branches: [main], so a PR based on a feature branch gets neither.claude-reviewhas no branch filter and will run, but its pass is a code opinion, not a test result.Not included, deliberately
The header's Add a project control (
rail-add-project) is an unlabeled folder-plus sitting beside the settings⋮. Reported as hard to find; it is a separate change and no decision covers it yet.🤖 Generated with Claude Code
https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw